Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

9장. 첫 에이전틱 코딩 — 버그 하나를 끝까지

8장에서 CLAUDE.md 와 권한 설정을 커밋했다.

이제 처음으로 코드를 고친다.

3장에서 개념으로 훑은 그 버그를,
이번에는 실제 세션으로 처음부터 끝까지 진행한다.


첫 작업은 고르는 것부터가 실력이다

무엇을 첫 작업으로 삼느냐가 결과의 절반이다.

좋은 첫 작업의 조건은 세 가지다.

조건이유
재현 가능하다고쳐졌는지 확인할 수 있다
영향 범위가 좁다Diff를 끝까지 읽을 수 있다
되돌리기 쉽다실패해도 손실이 없다

반대로 첫 작업으로 최악인 것들이 있다.

⚠️ 성능 개선, 컨벤션 일괄 정리, 원인 불명 간헐적 장애.

셋 다 “고쳐졌다” 를 판정할 기준이 없다.

우리 과제는 조건에 맞는다.

결제 취소 시 포인트가 두 번 환급된다


1️⃣ 계획 모드로 시작한다

Shift+Tab 으로 계획 모드에 들어간 뒤 작업을 준다.

결제 취소 시 포인트가 두 번 환급되는 버그가 있다.

관련 코드를 찾아서 원인을 설명하고, 수정 계획을 제시해줘.
아직 코드는 수정하지 마.

- 영향받는 취소 경로를 전부 확인해줘 (전체 취소, 부분 취소, 관리자 강제 취소)
- 기존 테스트를 수정하는 방향은 제외해줘

두 번째 제약이 3장에서 본 위험한 재시도를 미리 막는다.


2️⃣ Agent가 찾아온 것을 검토한다

> grep -rn "refundPoint" --include=*.kt

  point/PointRefundService.kt:42
  order/OrderCancelFacade.kt:88
  payment/PaymentCancelHandler.kt:61

이어서 호출 흐름 정리가 돌아온다.

OrderCancelFacade.cancel()
  ├─ PaymentCancelHandler.cancel()
  │     → PaymentCancelledEvent 발행
  │        └─ PointRefundListener.handle()  → 환급 (1)
  └─ PointRefundService.refund()            → 환급 (2)

원인: 이벤트 리스너와 Facade의 직접 호출이 중복

여기서 개발자가 할 일은 승인이 아니다.

의심이다.

PointRefundListener는 언제 추가됐어? git log로 확인해줘.
> git log --oneline -- src/main/kotlin/point/PointRefundListener.kt

  a3f9c21 feat: 결제 취소 이벤트 기반 포인트 환급 (3개월 전)

이 한 줄이 판단을 바꾼다.

세 달 전에 이벤트 방식이 추가되면서
Facade의 기존 직접 호출을 지우지 않은 것이다.

그러면 지울 쪽은 오래된 직접 호출이다.

🔥 이 확인을 건너뛰면 반대쪽을 지울 수도 있었다.

Agent는 코드만 보고, 사람은 이력을 본다.


3️⃣ 계획을 승인한다

수정 방향: OrderCancelFacade의 PointRefundService.refund() 직접 호출 제거
영향 범위: 전체 취소, 부분 취소, 관리자 강제 취소
검증: 재현 테스트 신규 1건 + 기존 취소 테스트 6건

동의하면 계획 모드를 벗어난다.

이 지점이 21장 Plan First의 실제 모습이다.


4️⃣ 재현 테스트를 먼저 만든다

먼저 이 버그를 재현하는 테스트를 추가해줘.
지금은 실패해야 정상이야.
@Test
fun `주문 취소 시 포인트 환급은 한 번만 발생한다`() {
    val order = 주문_생성(usedPoint = 1_000)

    orderCancelFacade.cancel(order.id)

    val histories = pointHistoryRepository
        .findAllByOrderId(order.id)
    assertThat(histories).hasSize(1)
}
> ./gradlew test --tests '*OrderCancelTest'

  주문 취소 시 포인트 환급은 한 번만 발생한다  FAILED
    expected size: 1 but was: 2

실패를 확인하는 이 단계를 생략하지 않는다.

실패하지 않는 재현 테스트는 아무것도 증명하지 않는다.


5️⃣ 수정하고 다시 돌린다

> ./gradlew test --tests '*OrderCancel*'

  BUILD SUCCESSFUL
  7 tests completed

여기서 끝내고 싶어진다.

아직 아니다.


6️⃣ Diff는 반드시 읽는다

git diff

이 단계가 에이전틱 코딩에서 가장 자주 생략되고,
가장 자주 후회하는 단계다.

세 가지를 확인한다.

확인
의도한 파일만 바뀌었나곁가지 수정이 섞였을 수 있다
테스트가 약해지지 않았나단정문 삭제, @Disabled 추가
관계없는 정리가 없나import 정렬, 포맷 변경이 섞이면 리뷰가 어려워진다

세 번째는 사소해 보이지만
Diff 200줄 중 190줄이 포맷 변경이면 리뷰는 사실상 불가능해진다.

발견하면 되돌린다.

포맷 변경은 되돌리고 로직 수정만 남겨줘.

7️⃣ 커밋은 작게

git add -p
git commit -m "fix: 주문 취소 시 포인트 이중 환급 수정

이벤트 기반 환급(a3f9c21) 도입 시 제거되지 않은
Facade의 직접 호출을 제거.
재현 테스트 추가."

커밋 메시지에 원인 커밋을 남겨두면
다음 사람이 이 판단을 다시 하지 않는다.

19장에서 말하는 외부화의 가장 값싼 형태다.


잘못됐을 때 되돌리는 법

세 가지 층이 있다.

상황방법
작업 중 방향이 틀렸다Esc 로 중단
수정이 마음에 안 든다git checkout -- <파일>
여러 파일이 엉켰다git stash 또는 브랜치 폐기

작업 전 git status 가 깨끗했다면
세 방법 모두 즉시 통한다.

그래서 6장에서 그 조건을 강조했다.


세션을 멈춰야 하는 신호

첫 작업에서 이런 상황이 오면 이어가지 않는다.

⚠️ 같은 수정을 두 번째로 반복한다
⚠️ 테스트를 통과시키려 검증을 약화시킨다
⚠️ 설명이 처음 설명과 달라진다

세 신호 모두 같은 뜻이다.

Context가 오염됐다.

/clear 로 새 Session을 시작하고,
방금 알아낸 사실만 정리해서 다시 넘긴다.

18장에서 이 판단 기준을 다룬다.


이 장의 핵심

  • 첫 작업은 재현 가능하고, 범위가 좁고, 되돌리기 쉬운 것으로 고른다
  • 성능 개선과 컨벤션 정리는 첫 작업으로 최악이다 — 판정 기준이 없다
  • 계획 모드에서 시작해 원인과 계획을 먼저 받는다
  • Agent는 코드를 보고, 사람은 git log 를 본다 — 이력이 판단을 바꾼다
  • 실패하지 않는 재현 테스트는 아무것도 증명하지 않는다
  • Diff를 읽지 않고 커밋하는 것은 에이전틱 코딩이 아니다
  • 커밋 메시지에 원인 커밋을 남기면 판단이 재사용된다
  • 같은 수정 반복 · 검증 약화 · 설명 변경은 Session을 끊으라는 신호다